systemd 强杀服务导致配置文件损坏的排查
一个由 systemd 托管的后台服务,重启后读到了自己损坏的 JSON 配置文件,无法启动。本文记录这次排查的完整过程:从报错日志确认文件损坏,复盘出两个损坏场景——直接断电,以及 systemd 停机超时后强杀进程导致写入中断,最后给出退出顺序与原子写入两个修复方向。对任何「服务退出时要落盘配置」的场景都适用。
现象
参数服务启动时报错,配置文件 parameter.json 解析失败:
[2026-05-27 16:20:34.254] [error] JSON parse failed: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal
[2026-05-27 16:20:34.254] [error] /opt/controller/cfg/parameter.json, Json file has duplicate keys.
[2026-05-27 16:20:34.254] [error] Failed to load parameter file: '/opt/controller/cfg/parameter.json'
[2026-05-27 16:20:36.244] [warning] Cannot save to JSON: JSON data is empty. 根因分析
启动时读到损坏的配置文件,初始化失败。参数服务随后尝试把内存中的参数落盘,但此时参数列表为空,打印 Cannot save to JSON: JSON data is empty. 后直接返回,没有执行写文件操作。
结合 JSON parse failed 的报错判断:文件不为空,但内容是不完整的 JSON——上一次退出时写了一半。问题从「这次为什么起不来」转化为「上次退出时谁打断了写入」。
损坏是怎么发生的
场景一:直接断电
断电瞬间配置文件正在写入,页缓存来不及刷盘,文件内容截断为不完整的 JSON。这个场景没有优雅退出流程可走,只能靠写入方式本身兜底(见文末原子写入)。
场景二:systemd 停机超时强杀
整套服务由一个 systemd 服务单元托管,systemctl stop 触发停止。systemd 按 KillMode=mixed 的规则执行停机:
- 向主进程发送 SIGTERM(信号 15),请求优雅退出
- 等待最多
TimeoutStopSec(该单元配置为 5s;不配置时默认 90s) - 超时后向控制组内所有残留进程发送 SIGKILL(信号 9)
这个单元的主进程是一个服务管理进程,它收到 SIGTERM 后的设计流程是:先回收自身资源,再向各业务服务发送退出信号,等待 2s,最后调用 sync() 强制落盘。
实际日志显示,它自身的资源回收花了接近 5s。等它开始给业务服务发退出信号时,systemd 的超时窗口已经用完,控制组内所有进程直接被 SIGKILL 强杀,业务服务的配置写入被中断:
[15:32:41.048] 收到 SIGTERM,开始资源回收
[15:32:43.023] 第一个子模块回收完成(~2s)
[15:32:44.449] 采集服务回收完成(~1.4s)
[15:32:45.969] 资源回收完毕,开始向业务服务发送退出信号 ← 距 SIGTERM 已过 ~5s
[~15:32:46] systemd 超时,SIGKILL 强杀全部进程 从 SIGTERM 到开始发送退出信号耗时约 5s,业务服务没有时间完成退出和落盘,写到一半的配置文件就此留在磁盘上。
修复与预防
三个方向,前两个针对本例根因,第三个针对排查过程中的盲区:
- 退出顺序:管理进程应先向各业务服务发送退出信号,再做自身资源回收;或者压缩自身回收耗时,确保在
TimeoutStopSec窗口内给业务服务留足安全退出的时间。停机窗口实在不够时,也可以直接调大TimeoutStopSec - 原子写入:配置文件落盘用 write-to-temp + rename 模式——先写临时文件,写完并
fsync后用rename()替换原文件。rename在同一文件系统上是原子操作,无论进程在哪一步被杀,磁盘上的文件要么是旧版本、要么是完整的新版本,不会出现半截文件 - 让断电可观测:本例中直接断电的场景没有收到电源管理组件的异常上报,缺乏直接证据,只能靠日志推理。关键事件(断电、异常关机)有上报渠道,排查时能少走弯路
要点
- systemd 停机不是无限等待:
TimeoutStopSec超时后 SIGKILL 直接发给整个控制组,优雅退出流程必须在这个窗口内完成 - 管理进程自身的清理逻辑会挤占被管理服务的退出时间;退出顺序上先通知别人,再收拾自己
- 配置类文件一律用临时文件 + rename 的原子写入,把「写到一半被杀」从不可恢复变成无害